/****************************************************************************
 * Copyright (c) 2009 EclipseSource and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *   EclipseSource - initial API and implementation
 *
 * SPDX-License-Identifier: EPL-2.0
 *****************************************************************************/
package org.eclipse.ecf.remoteservice.rest.client;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import org.apache.http.*;
import org.apache.http.auth.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.AbstractHttpMessage;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.eclipse.ecf.core.security.*;
import org.eclipse.ecf.core.util.ECFException;
import org.eclipse.ecf.remoteservice.IRemoteCall;
import org.eclipse.ecf.remoteservice.IRemoteService;
import org.eclipse.ecf.remoteservice.client.*;
import org.eclipse.ecf.remoteservice.rest.IRestCall;
import org.eclipse.ecf.remoteservice.rest.RestException;

/**
 * This class represents a REST service from the client side of view. So a
 * RESTful web service can be accessed via the methods provided by this class.
 * Mostly the methods are inherited from {@link IRemoteService}.
 */
public class RestClientService extends AbstractRestClientService {

	public static final int socketTimeout = Integer.parseInt(System.getProperty("org.eclipse.ecf.remoteservice.rest.RestClientService.socketTimeout", "-1")); //$NON-NLS-1$ //$NON-NLS-2$
	public static final int connectRequestTimeout = Integer.parseInt(System.getProperty("org.eclipse.ecf.remoteservice.rest.RestClientService.connectRequestTimeout", "-1")); //$NON-NLS-1$ //$NON-NLS-2$
	public static final int connectTimeout = Integer.parseInt(System.getProperty("org.eclipse.ecf.remoteservice.rest.RestClientService.connectTimeout", "-1")); //$NON-NLS-1$ //$NON-NLS-2$

	protected final static int DEFAULT_RESPONSE_BUFFER_SIZE = 1024;

	protected final static String DEFAULT_HTTP_CONTENT_CHARSET = "UTF-8"; //$NON-NLS-1$

	protected HttpClient httpClient;
	protected int responseBufferSize = DEFAULT_RESPONSE_BUFFER_SIZE;

	public RestClientService(RestClientContainer container, RemoteServiceClientRegistration registration) {
		super(container, registration);
		this.httpClient = createHttpClient();
	}

	protected HttpClient createHttpClient() {
		return HttpClientBuilder.create().build();
	}

	private boolean isResponseOk(HttpResponse response) {
		int isOkCode = response.getStatusLine().getStatusCode() - 200;
		return (isOkCode >= 0 && isOkCode < 100);
	}

	protected HttpGet createGetMethod(String uri) {
		return new HttpGet(uri);
	}

	protected HttpPost createPostMethod(String uri) {
		return new HttpPost(uri);
	}

	protected HttpPut createPutMethod(String uri) {
		return new HttpPut(uri);
	}

	protected HttpDelete createDeleteMethod(String uri) {
		return new HttpDelete(uri);
	}

	/**
	 * @since 2.6
	 */
	protected HttpPatch createPatchMethod(String uri) {
		return new HttpPatch(uri);
	}

	protected HttpRequestBase createAndPrepareHttpMethod(UriRequest request) {
		HttpRequestBase httpMethod = null;
		String uri = request.getUri();
		final IRemoteCallable callable = request.getRemoteCallable();
		IRemoteCallableRequestType requestType = (callable == null) ? new HttpGetRequestType() : callable.getRequestType();
		if (requestType instanceof HttpGetRequestType)
			httpMethod = createGetMethod(uri);
		else if (requestType instanceof HttpPatchRequestType)
			httpMethod = createPatchMethod(uri);
		else if (requestType instanceof HttpPostRequestType)
			httpMethod = createPostMethod(uri);
		else if (requestType instanceof HttpPutRequestType)
			httpMethod = createPutMethod(uri);
		else if (requestType instanceof HttpDeleteRequestType)
			httpMethod = createDeleteMethod(uri);
		// all prepare HttpMethod
		prepareHttpMethod(httpMethod, request.getRemoteCall(), callable);
		return httpMethod;
	}

	/**
	 * Calls the Rest service with given URL of IRestCall. The returned value is
	 * the response body as an InputStream.
	 * 
	 * @param call
	 *            The remote call to make.  Must not be <code>null</code>.
	 * @param callable
	 *            The callable with default parameters to use to make the call.
	 * @return The InputStream of the response body or <code>null</code> if an
	 *         error occurs.
	 */
	protected Object invokeRemoteCall(final IRemoteCall call, final IRemoteCallable callable) throws ECFException {
		trace("invokeRemoteCall", "call=" + call + ";callable=" + callable); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		String endpointUri = prepareEndpointAddress(call, callable);
		trace("invokeRemoteCall", "prepared endpoint=" + endpointUri); //$NON-NLS-1$ //$NON-NLS-2$
		UriRequest urirequest = createUriRequest(endpointUri, call, callable);
		// If the request
		HttpRequestBase httpMethod = (urirequest == null) ? createAndPrepareHttpMethod(endpointUri, call, callable) : createAndPrepareHttpMethod(urirequest);
		trace("invokeRemoteCall", "executing httpMethod" + httpMethod); //$NON-NLS-1$ //$NON-NLS-2$
		// execute method
		byte[] responseBody = null;
		int responseCode = 500;
		HttpResponse response = null;
		try {
			response = httpClient.execute(httpMethod);
			trace("invokeRemoteCall", "httpMethod executed. response=" + response); //$NON-NLS-1$ //$NON-NLS-2$
			responseCode = response.getStatusLine().getStatusCode();
			if (isResponseOk(response)) {
				// Get responseBody as String
				responseBody = getResponseAsBytes(response);
			} else {
				// If this method returns true, we should retrieve the response body
				if (retrieveErrorResponseBody(response)) {
					responseBody = getResponseAsBytes(response);
				}
				// Now pass to the exception handler
				handleException("Http response not OK.  httpMethod=" + httpMethod + " responseCode=" + Integer.valueOf(responseCode), null, responseCode, responseBody); //$NON-NLS-1$ //$NON-NLS-2$
			}
		} catch (IOException e) {
			handleException("RestClientService transport IOException", e, responseCode); //$NON-NLS-1$
		}
		Object result = null;
		try {
			Map responseHeaders = convertResponseHeaders(response.getAllHeaders());
			trace("processResponse", "httpMethod=" + httpMethod + ";call=" + call + ";callable=" + callable + ";responseHeaders=" + responseHeaders + ";responseBody=" + responseBody); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
			result = processResponse(endpointUri, call, callable, responseHeaders, responseBody);
		} catch (NotSerializableException e) {
			handleException("Exception deserializing response.  httpMethod=" + httpMethod + " responseCode=" + Integer.valueOf(responseCode), e, responseCode); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return result;
	}

	protected boolean retrieveErrorResponseBody(HttpResponse response) {
		// XXX this needs to be defined differently for 
		return false;
	}

	protected byte[] getResponseAsBytes(HttpResponse response) throws IOException {
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		response.getEntity().writeTo(os);
		return os.toByteArray();
	}

	/*
	 * @deprecated
	 */
	protected void setupTimeouts(HttpClient httpClient, IRemoteCall call, IRemoteCallable callable) {
		// No longer used
	}

	private Map convertResponseHeaders(Header[] headers) {
		Map result = new HashMap();
		if (headers == null)
			return result;
		for (int i = 0; i < headers.length; i++) {
			String name = headers[i].getName();
			String value = headers[i].getValue();
			result.put(name, value);
		}
		return result;
	}

	protected void addRequestHeaders(AbstractHttpMessage httpMethod, IRemoteCall call, IRemoteCallable callable) {
		// Add request headers from the callable
		Map requestHeaders = (callable.getRequestType() instanceof AbstractRequestType) ? ((AbstractRequestType) callable.getRequestType()).getDefaultRequestHeaders() : new HashMap();
		if (requestHeaders == null)
			requestHeaders = new HashMap();

		if (call instanceof IRestCall) {
			Map callHeaders = ((IRestCall) call).getRequestHeaders();
			if (callHeaders != null)
				requestHeaders.putAll(requestHeaders);
		}

		Set keySet = requestHeaders.keySet();
		Object[] headers = keySet.toArray();
		for (int i = 0; i < headers.length; i++) {
			String key = (String) headers[i];
			String value = (String) requestHeaders.get(key);
			httpMethod.addHeader(key, value);
		}
	}

	protected HttpRequestBase createAndPrepareHttpMethod(String uri, IRemoteCall call, IRemoteCallable callable) throws RestException {
		HttpRequestBase httpMethod = null;

		IRemoteCallableRequestType requestType = callable.getRequestType();
		if (requestType == null)
			throw new RestException("Request type for call cannot be null"); //$NON-NLS-1$
		try {
			if (requestType instanceof HttpGetRequestType) {
				httpMethod = prepareGetMethod(uri, call, callable);
			} else if (requestType instanceof HttpPatchRequestType) {
				httpMethod = preparePatchMethod(uri, call, callable);
			} else if (requestType instanceof HttpPostRequestType) {
				httpMethod = preparePostMethod(uri, call, callable);
			} else if (requestType instanceof HttpPutRequestType) {
				httpMethod = preparePutMethod(uri, call, callable);
			} else if (requestType instanceof HttpDeleteRequestType) {
				httpMethod = prepareDeleteMethod(uri, call, callable);
			} else {
				throw new RestException("HTTP method " + requestType + " not supported"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		} catch (NotSerializableException e) {
			String message = "Could not serialize parameters for uri=" + uri + " call=" + call + " callable=" + callable; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			logException(message, e);
			throw new RestException(message);
		} catch (UnsupportedEncodingException e) {
			String message = "Could not serialize parameters for uri=" + uri + " call=" + call + " callable=" + callable; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			logException(message, e);
			throw new RestException(message);
		}

		prepareHttpMethod(httpMethod, call, callable);
		return httpMethod;
	}

	protected void prepareHttpMethod(HttpRequestBase httpMethod, IRemoteCall call, IRemoteCallable callable) {
		// add additional request headers
		addRequestHeaders(httpMethod, call, callable);
		// handle authentication
		setupAuthenticaton(httpClient, httpMethod);
		// setup http method config (redirects, timeouts, etc)
		setupHttpMethod(httpMethod, call, callable);
	}

	protected void setupHttpMethod(HttpRequestBase httpMethod, IRemoteCall call, IRemoteCallable callable) {

		RequestConfig defaultRequestConfig = httpMethod.getConfig();
		RequestConfig.Builder updatedRequestConfigBuilder = (defaultRequestConfig == null) ? RequestConfig.custom() : RequestConfig.copy(defaultRequestConfig);
		// setup to allow regular and circular redirects
		updatedRequestConfigBuilder.setCircularRedirectsAllowed(true);
		updatedRequestConfigBuilder.setRedirectsEnabled(true);

		int sTimeout = socketTimeout;
		int scTimeout = connectTimeout;
		int scrTimeout = connectRequestTimeout;

		long callTimeout = call.getTimeout();
		if (callTimeout == IRemoteCall.DEFAULT_TIMEOUT)
			callTimeout = callable.getDefaultTimeout();

		if (callTimeout != IRemoteCall.DEFAULT_TIMEOUT) {
			sTimeout = scTimeout = scrTimeout = new Long(callTimeout).intValue();
		}
		updatedRequestConfigBuilder.setSocketTimeout(sTimeout);
		updatedRequestConfigBuilder.setConnectTimeout(scTimeout);
		updatedRequestConfigBuilder.setConnectionRequestTimeout(scrTimeout);

		httpMethod.setConfig(updatedRequestConfigBuilder.build());
	}

	/**
	 * @throws RestException  
	 */
	protected HttpRequestBase prepareDeleteMethod(String uri, IRemoteCall call, IRemoteCallable callable) throws RestException {
		return new HttpDelete(uri);
	}

	protected HttpRequestBase preparePutMethod(String uri, IRemoteCall call, IRemoteCallable callable) throws NotSerializableException, UnsupportedEncodingException {
		HttpPut result = new HttpPut(uri);
		HttpPutRequestType putRequestType = (HttpPutRequestType) callable.getRequestType();

		IRemoteCallParameter[] defaultParameters = callable.getDefaultParameters();
		Object[] parameters = call.getParameters();

		if (putRequestType.useRequestEntity()) {
			if (defaultParameters != null && defaultParameters.length > 0 && parameters != null && parameters.length > 0) {
				HttpEntity requestEntity = putRequestType.generateRequestEntity(uri, call, callable, defaultParameters[0], parameters[0]);
				result.setEntity(requestEntity);
			}
		} else {
			NameValuePair[] params = toNameValuePairs(uri, call, callable);
			if (params != null) {
				result.setEntity(getUrlEncodedFormEntity(Arrays.asList(params), putRequestType));
			}
		}
		return result;
	}

	/**
	 * @throws UnsupportedEncodingException 
	 * @throws ECFException  
	 * @since 2.6
	 */
	protected HttpRequestBase preparePatchMethod(String uri, IRemoteCall call, IRemoteCallable callable) throws NotSerializableException, UnsupportedEncodingException {
		HttpPatch result = new HttpPatch(uri);
		HttpPostRequestType postRequestType = (HttpPostRequestType) callable.getRequestType();

		IRemoteCallParameter[] defaultParameters = callable.getDefaultParameters();
		Object[] parameters = call.getParameters();
		if (postRequestType.useRequestEntity()) {
			if (defaultParameters != null && defaultParameters.length > 0 && parameters != null && parameters.length > 0) {
				HttpEntity requestEntity = postRequestType.generateRequestEntity(uri, call, callable, defaultParameters[0], parameters[0]);
				result.setEntity(requestEntity);
			}
		} else {
			NameValuePair[] params = toNameValuePairs(uri, call, callable);
			if (params != null) {
				result.setEntity(getUrlEncodedFormEntity(Arrays.asList(params), postRequestType));
			}
		}
		return result;
	}

	/**
	 * @throws UnsupportedEncodingException 
	 * @throws ECFException  
	 */
	protected HttpRequestBase preparePostMethod(String uri, IRemoteCall call, IRemoteCallable callable) throws NotSerializableException, UnsupportedEncodingException {
		HttpPost result = new HttpPost(uri);
		HttpPostRequestType postRequestType = (HttpPostRequestType) callable.getRequestType();

		IRemoteCallParameter[] defaultParameters = callable.getDefaultParameters();
		Object[] parameters = call.getParameters();
		if (postRequestType.useRequestEntity()) {
			if (defaultParameters != null && defaultParameters.length > 0 && parameters != null && parameters.length > 0) {
				HttpEntity requestEntity = postRequestType.generateRequestEntity(uri, call, callable, defaultParameters[0], parameters[0]);
				result.setEntity(requestEntity);
			}
		} else {
			NameValuePair[] params = toNameValuePairs(uri, call, callable);
			if (params != null) {
				result.setEntity(getUrlEncodedFormEntity(Arrays.asList(params), postRequestType));
			}
		}
		return result;
	}

	/**
	 * @throws NotSerializableException 
	 * @throws ECFException  
	 */
	protected HttpRequestBase prepareGetMethod(String uri, IRemoteCall call, IRemoteCallable callable) throws NotSerializableException {
		NameValuePair[] params = toNameValuePairs(uri, call, callable);
		URI httpURI = null;
		try {
			URIBuilder builder = new URIBuilder(uri);
			if (params != null)
				for (int i = 0; i < params.length; i++)
					builder.addParameter(params[i].getName(), params[i].getValue());
			httpURI = builder.build();
		} catch (URISyntaxException e1) {
			throw new NotSerializableException("uri=" + uri + " does not have proper URI syntax"); //$NON-NLS-1$//$NON-NLS-2$
		}
		return new HttpGet(httpURI);
	}

	protected UrlEncodedFormEntity getUrlEncodedFormEntity(List list, AbstractEntityRequestType postRequestType) throws UnsupportedEncodingException {
		if (postRequestType.defaultCharset != null) {
			return new UrlEncodedFormEntity(list, postRequestType.defaultCharset);
		}
		return new UrlEncodedFormEntity(list);
	}

	protected NameValuePair[] toNameValuePairs(String uri, IRemoteCall call, IRemoteCallable callable) throws NotSerializableException {
		IRemoteCallParameter[] restParameters = prepareParameters(uri, call, callable);
		List nameValueList = new ArrayList();
		if (restParameters != null) {
			for (int i = 0; i < restParameters.length; i++) {
				String parameterValue = null;
				Object o = restParameters[i].getValue();
				if (o instanceof String) {
					parameterValue = (String) o;
				} else if (o != null) {
					parameterValue = o.toString();
				}
				if (parameterValue != null) {
					nameValueList.add(new BasicNameValuePair(restParameters[i].getName(), parameterValue));
				}
			}
		}
		return (NameValuePair[]) nameValueList.toArray(new NameValuePair[nameValueList.size()]);
	}

	protected void setupAuthenticaton(HttpClient httpClient, HttpRequestBase method) {
		IConnectContext connectContext = container.getConnectContextForAuthentication();
		if (connectContext != null) {
			NameCallback nameCallback = new NameCallback(""); //$NON-NLS-1$
			ObjectCallback passwordCallback = new ObjectCallback();
			Callback[] callbacks = new Callback[] {nameCallback, passwordCallback};
			CallbackHandler callbackHandler = connectContext.getCallbackHandler();
			if (callbackHandler == null)
				return;
			try {
				callbackHandler.handle(callbacks);
				String username = nameCallback.getName();
				String password = (String) passwordCallback.getObject();
				Credentials credentials = new UsernamePasswordCredentials(username, password);
				method.addHeader(new BasicScheme().authenticate(credentials, method, new BasicHttpContext()));
			} catch (IOException e) {
				logException("IOException setting credentials for rest httpclient", e); //$NON-NLS-1$
			} catch (UnsupportedCallbackException e) {
				logException("UnsupportedCallbackException setting credentials for rest httpclient", e); //$NON-NLS-1$
			} catch (AuthenticationException e) {
				logException("AuthenticationException setting credentials for rest httpclient", e); //$NON-NLS-1$
			}

		}
	}

}
